﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace MemoryViewControl
{
  public partial class MemoryHexEditorControl : UserControl
  {
    public struct SectionRenderInfo
    {
      public int maxLines;
      public int visibleLines;
      public int startLineIndex;
    };

    public struct LineRenderInfo
    {
      public MemorySection section;
      public uint address;
      public bool invalidSection;
      public bool invalid;
    };

    public class Style
    {
    }

    public class GetMemoryEventArgs : EventArgs { public List<MemorySpace.RangeEntry> entries; };
    public class MemoryChangedEventArgs : EventArgs { public uint address; public int length; public byte[] data; };
    public class HoverAddressChangedEventArgs : EventArgs { public bool valid; public uint address; };

    public delegate void GetMemoryHandler(object sender, GetMemoryEventArgs e);
    public delegate void MemoryChangedHandler(object sender, MemoryChangedEventArgs e);
    public delegate void HoverAddressChangedHandler(object sender, HoverAddressChangedEventArgs e);

    public event GetMemoryHandler GetMemory;
    public event MemoryChangedHandler MemoryChanged;
    public event HoverAddressChangedHandler HoverAddressChanged;

    public event PropertyChangedEventHandler PropertyChanged;

    private MemorySpace m_memorySpace = new MemorySpace();

    private int m_maxDisplayLines = 0;
    private int m_maxDataLines = 0;
    private uint m_offset = 0;
    private int m_grouping = 1;
    private int m_columns = 16;
    private int m_pitch = 32;
    private Font m_memoryFont = new Font(FontFamily.GenericMonospace, 12, FontStyle.Regular);//, GraphicsUnit.Pixel);
    private float m_characterWidth = 0;
    private int m_bufferLines = 4;

    private float m_renderAddressWidth = 0.0f;
    private float m_renderAddressStride = 0.0f;
    private float m_renderColumnWidth = 0.0f;
    private float m_renderColumnStride = 0.0f;

    private int m_hoverLine = 0;
    private int m_hoverColumn = 0;
    private int m_hoverNibble = 0;
    private uint m_hoverAddress = 0;
    private bool m_hoverAddressValid = false;

    private SectionRenderInfo[] m_sectionRenderInfo = new SectionRenderInfo[0];
    private LineRenderInfo[] m_lineRenderInfo = new LineRenderInfo[0];
    private int m_maxBits = 8;

    private bool m_renderInfoDirty = true;
    private bool m_renderLabels = true;

    private uint m_selectionStart = 0;
    private uint m_selectionNibble = 0;
    private uint m_selectionLength = 0;

    private bool m_renderInvalid = false;
    private bool m_renderHidden = false;

    private Point m_mousePos = new Point();


    private Brush[] m_highlightBrushes = {
        Brushes.Blue, Brushes.Green, Brushes.Red, Brushes.Aqua,
        Brushes.LightSeaGreen, Brushes.Turquoise, Brushes.LightCyan, Brushes.LightBlue,
        Brushes.White, Brushes.Green, Brushes.Red, Brushes.Aqua,
        Brushes.LightSeaGreen, Brushes.Turquoise, Brushes.LightCyan, Brushes.LightBlue,
    };

    public Brush GetHighlightBrush(int index) { return m_highlightBrushes[index]; }
    public void SetHighlightBrush(int index, Brush brush) { if ((index > 0) && (brush != null)) { m_highlightBrushes[index] = brush; } }

    protected void OnPropertyChanged(string propertyName)
    {
      if (PropertyChanged != null)
      {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
      }
    }

    protected void OnHoverAddressChanged(bool valid, uint address)
    {
      if (HoverAddressChanged != null)
      {
        HoverAddressChangedEventArgs args = new HoverAddressChangedEventArgs();
        args.valid = valid;
        args.address = address;
        HoverAddressChanged(this, args);
      }
    }

    public void RedrawMemory(bool b)
    {
      m_renderInfoDirty = b;
      Invalidate();
    }

    private void MemorySpace_Redraw(object sender, EventArgs args)
    {
      RedrawMemory(true);
    }

    private void MemorySpace_PropertyChanged(object sender, PropertyChangedEventArgs args)
    {
      switch (args.PropertyName)
      {
      default:
        break;
      }

      CalculateSizes();
    }

    #region Properties
    public MemorySpace MemorySpace
    {
      get { return m_memorySpace; }
      set
      {
        if (m_memorySpace != value)
        {
          if (m_memorySpace != null)
          {
            m_memorySpace.MemoryChanged -= new EventHandler(MemorySpace_Redraw);
          }

          m_memorySpace = value;

          if (m_memorySpace != null)
          {
            m_memorySpace.MemoryChanged += new EventHandler(MemorySpace_Redraw);
          }
        }
      }
    }

    public int Columns
    {
      get { return m_columns; }
      set { if (m_columns != value) { m_columns = value; OnPropertyChanged("Columns"); RedrawMemory(true); } }
    }

    public int Grouping
    {
      get { return m_grouping; }
      set { if (m_grouping != value) { m_grouping = value; OnPropertyChanged("Grouping"); RedrawMemory(true); } }
    }

    public Font MemoryFont
    {
      get { return m_memoryFont; }
      set { if (m_memoryFont != value) { m_memoryFont = value; m_characterWidth = 0; OnPropertyChanged("MemoryFont"); RedrawMemory(true); } }
    }

    [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public uint Offset
    {
      get { return m_offset; }
      set
      {
        if (m_offset != value)
        {
          m_offset = value;
          UpdateScrollBarPosition();
          OnPropertyChanged("Offset");
          RedrawMemory(true);
        }
      }
    }

    [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public bool RenderHiddenBytes
    {
      get { return m_renderHidden; }
      set { if (m_renderHidden != value) { m_renderHidden = value; OnPropertyChanged("RenderHiddenBytes"); RedrawMemory(true); } }
    }

    [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public bool RenderInvalidBytes
    {
      get { return m_renderInvalid; }
      set { if (m_renderInvalid != value) { m_renderInvalid = value; OnPropertyChanged("RenderInvalidBytes"); RedrawMemory(true); } }
    }

    [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public uint SelectionStart
    {
      get { return m_selectionStart; }
      set { if ((value != m_selectionStart) && (value < m_memorySpace.AddressMax)) { m_selectionStart = value; RedrawMemory(false); } }
    }

    [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public uint SelectionLength
    {
      get { return m_selectionLength; }
      set { if ((value != m_selectionLength) && (value < m_memorySpace.AddressMax)) { m_selectionLength = value; RedrawMemory(false); } }
    }

    [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public MemorySpace.ByteInfo[] Selection
    {
      get
      {
        MemorySpace.ByteInfo[] selection = new MemorySpace.ByteInfo[m_selectionLength];
        m_memorySpace.GetBytes(m_selectionStart, ref selection);
        return selection;
      }
      set
      {
        ushort[] selection = new ushort[value.Length];
        for (int i = 0; i < value.Length; ++i)
        {
          selection[i] = value[i].bits;
        }
        m_memorySpace.UpdateMemory(m_selectionStart, selection, false);
      }
    }

    [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public byte[] SelectionBytes
    {
      get
      {
        MemorySpace.ByteInfo[] bytes = new MemorySpace.ByteInfo[m_selectionLength];
        byte[] selection = new byte[m_selectionLength];
        m_memorySpace.GetBytes(m_selectionStart, ref bytes);
        for (int i = 0; i < m_selectionLength; ++i)
        {
          selection[i] = (byte)bytes[i].bits;
        }
        return selection;
      }
      set
      {
        m_memorySpace.UpdateMemory(m_selectionStart, value, false);
      }
    }

    public bool HoverAddressValid { get { return m_hoverAddressValid; } }
    public uint HoverAddress { get { return m_hoverAddress; } }
    #endregion

    public MemoryHexEditorControl()
    {
      DoubleBuffered = true;
      SetStyle(ControlStyles.Selectable, true);
      SetStyle(ControlStyles.UserMouse, true);

      InitializeComponent();
    }

    protected void SetSelection()
    {
    }

    protected override void OnPaint(PaintEventArgs pe)
    {
      base.OnPaint(pe);
      Graphics graphics = pe.Graphics;

      //// Draw a dotted line inside the client rectangle
      //if (ShowFocusCues && ContainsFocus)
      //{
      //  Rectangle r = ClientRectangle;
      //  r.Inflate(-4, -4);
      //  r.Width--;
      //  r.Height--;
      //  Pen p = new Pen(Color.Black, 1);
      //  p.DashStyle = System.Drawing.Drawing2D.DashStyle.Dot;
      //  graphics.DrawRectangle(p, r);
      //}

      graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
      graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
      //graphics.TextContrast = 10;
      graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.Default;

      //graphics.Clear(Color.CornflowerBlue);

      CalculateSizes();

      int nibbles = m_maxBits / 4;
      float fNibbles = (float)nibbles;

      SizeF sizeThreeChars = graphics.MeasureString("000", m_memoryFont);
      SizeF sizeTwoChars = graphics.MeasureString("00", m_memoryFont);
      SizeF sizeOneChar = graphics.MeasureString("0", m_memoryFont);
      SizeF stringSize = sizeTwoChars - sizeOneChar;

      m_characterWidth = stringSize.Width;

      float fontHeight = m_memoryFont.Height;

      float characterPadding = (sizeOneChar.Width - m_characterWidth) / 2.0f;

      float gapWidth = 1.0f * m_characterWidth;

      // save these off
      m_renderAddressWidth = ((float)m_memorySpace.AddressNibbles + 2.0f) * m_characterWidth;
      m_renderAddressStride = m_renderAddressWidth + gapWidth;
      m_renderColumnWidth = (m_grouping * fNibbles) * m_characterWidth;
      m_renderColumnStride = m_renderColumnWidth + gapWidth;


      int maxChars = (int)((m_memorySpace.AddressNibbles + 3) + (m_pitch * (nibbles + 1)) + (1 + m_pitch));
      StringBuilder sbInvalid = new StringBuilder(maxChars);
      StringBuilder sbDirty = new StringBuilder(maxChars);
      StringBuilder sbChanged = new StringBuilder(maxChars);
      StringBuilder sbNormal = new StringBuilder(maxChars);

      MemorySpace.ByteInfo[] bytes = new MemorySpace.ByteInfo[m_pitch];

      char[] byteChars = new char[8];

      string addressFormatString = string.Concat("0x{0:X", m_memorySpace.AddressNibbles, "}");

      float y = 0.0f;

      for (int line = 0; line < m_maxDisplayLines; ++line)
      {
        LineRenderInfo lri = m_lineRenderInfo[line];
        uint lineOffset = lri.address;

        if (lineOffset >= m_memorySpace.AddressMax)
          break;

        float x = 0.0f;

        //if (line == m_hoverLine)
        //{
        //  graphics.DrawRectangle(Pens.Black, x, y, ClientSize.Width, m_memoryFont.Height);

        //  if (m_hoverColumn >= 0)
        //  {
        //    float x2 = m_renderAddressStride + (m_hoverColumn * m_characterWidth);

        //    graphics.DrawRectangle(Pens.Violet, x2, y, m_characterWidth, m_memoryFont.Height);
        //  }
        //}

        MemorySection section = lri.section;

        if (lri.invalidSection || (section != null))
        {
          sbNormal.Length = 0;

          if (section != null)
          {
            sbNormal.Append("[");
            sbNormal.AppendFormat(addressFormatString, section.address);
            sbNormal.Append(" - ");
            sbNormal.AppendFormat(addressFormatString, section.EndAddress - 1);
            sbNormal.AppendFormat("] '{0}'", section.Name);
          }
          else
          {
            sbNormal.Append("[INVALID MEMORY]");
          }

          graphics.DrawString(sbNormal.ToString(), m_memoryFont, Brushes.Green, x, y);
          y += fontHeight;

          continue;
        }

        if (lri.invalid)
        {
          sbNormal.Length = 0;

          sbNormal.Append("[OOPS, DONT LOOK]");

          graphics.DrawString(sbNormal.ToString(), m_memoryFont, Brushes.OliveDrab, x, y);
          y += fontHeight;

          continue;
        }

        m_memorySpace.GetBytes(lineOffset, ref bytes);

        sbInvalid.Length = 0;
        sbDirty.Length = 0;
        sbChanged.Length = 0;
        sbNormal.Length = 0;

        graphics.DrawString(string.Format(addressFormatString, lineOffset), m_memoryFont, Brushes.Black, x, y);

        x += m_renderAddressStride;

        // highlight
        for (uint b = 0; b < m_pitch; ++b)
        {
          uint mem = lineOffset + b;
          if (mem >= m_memorySpace.AddressMax)
            break;

          MemorySpace.ByteInfo bi = bytes[b];
          bool notHidden = m_renderHidden || ((bi.flags & MemorySpace.ByteInfo.Flags.Hidden) == 0);

          if (notHidden)
          {
            int highlight = bi.HighlightIndex;
            bool selected = ((mem >= m_selectionStart) && (mem < (m_selectionStart + m_selectionLength)));
            bool nextSel = (mem + 1 < (m_selectionStart + m_selectionLength));
            int columns = ((int)b * nibbles) + ((int)b / m_grouping);

            bool bColumnEnd = (((m_grouping == 1) || (b > 0)) && (b % m_grouping == 0));
            bool lastColumn = (b == (m_pitch - 1));

            float x1 = x + (columns * m_characterWidth);
            float width = characterPadding + (fNibbles * m_characterWidth) + characterPadding;

            if (selected && ContainsFocus)
            {
              //graphics.DrawRectangle(Pens.White, x1, y, width, fontHeight);

              if (bColumnEnd && nextSel && !lastColumn)
              {
                width += m_characterWidth;
              }

              graphics.FillRectangle(m_highlightBrushes[0], x1, y, width, fontHeight);
            }
            else if (highlight != 0)
            {
              graphics.FillRectangle(m_highlightBrushes[highlight], x1, y, width, fontHeight);
            }

            // crappy text caret
            if (mem == (m_selectionStart + Math.Max(1, m_selectionLength) - 1))
            {
              using (Pen pen = new Pen(Color.White, 3))
              {
                graphics.DrawLine(pen, x1, y, x1, y + fontHeight);
              }

              using (Pen pen = new Pen(Color.Black, 1))
              {
                graphics.DrawLine(pen, x1, y, x1, y + fontHeight);
              }
            }
          }
        }

        // numbers
        for (uint b = 0; b < m_pitch; ++b)
        {
          uint mem = lineOffset + b;
          if (mem >= m_memorySpace.AddressMax)
            break;

          bool bColumnEnd = ((b > 0) && (b % m_grouping == 0));

          int columns = ((int)b * nibbles) + ((int)b / m_grouping);

          MemorySpace.ByteInfo bi = bytes[b];

          FillHexString(bi.bits, ref byteChars, nibbles);
          if ((bi.flags & MemorySpace.ByteInfo.Flags.Hidden) != 0)
          {
            // hidden!
            if (m_renderHidden)
            {
              sbDirty.Append(' ', columns - sbDirty.Length);
              sbDirty.Append(byteChars, 0, nibbles);
            }
          }
          else if ((bi.flags & MemorySpace.ByteInfo.Flags.Invalid) != 0)
          {
            sbInvalid.Append(' ', columns - sbInvalid.Length);
            sbInvalid.Append('?', nibbles);
          }
          else if ((bi.flags & MemorySpace.ByteInfo.Flags.Dirty) != 0)
          {
            sbDirty.Append(' ', columns - sbDirty.Length);
            sbDirty.Append(byteChars, 0, nibbles);
          }
          else if ((bi.flags & MemorySpace.ByteInfo.Flags.Changed) != 0)
          {
            sbChanged.Append(' ', columns - sbChanged.Length);
            sbChanged.Append(byteChars, 0, nibbles);
          }
          else
          {
            sbNormal.Append(' ', columns - sbNormal.Length);
            sbNormal.Append(byteChars, 0, nibbles);
          }
        }

        // text
        for (uint b = 0; b < m_pitch; ++b)
        {
          uint mem = lineOffset + b;
          if (mem >= m_memorySpace.AddressMax)
            break;

          bool bColumnEnd = ((b > 0) && (b % m_grouping == 0));

          int columns = (int)((m_pitch * nibbles) + (m_pitch / m_grouping) + (b));

          MemorySpace.ByteInfo bi = bytes[b];

          char c = ByteToPrintableChar((byte)bi.bits);
          if ((bi.flags & MemorySpace.ByteInfo.Flags.Hidden) != 0)
          {
            // hidden!
            if (m_renderHidden)
            {
              sbDirty.Append(' ', columns - sbDirty.Length);
              sbDirty.Append(c);
            }
          }
          else if ((bi.flags & MemorySpace.ByteInfo.Flags.Invalid) != 0)
          {
            sbInvalid.Append(' ', columns - sbInvalid.Length);
            sbInvalid.Append('?');
          }
          else if ((bi.flags & MemorySpace.ByteInfo.Flags.Dirty) != 0)
          {
            sbDirty.Append(' ', columns - sbDirty.Length);
            sbDirty.Append(c);
          }
          else if ((bi.flags & MemorySpace.ByteInfo.Flags.Changed) != 0)
          {
            sbChanged.Append(' ', columns - sbChanged.Length);
            sbChanged.Append(c);
          }
          else
          {
            sbNormal.Append(' ', columns - sbNormal.Length);
            sbNormal.Append(c);
          }
        }

        if (sbInvalid.Length != 0)
        {
          graphics.DrawString(sbInvalid.ToString(), m_memoryFont, Brushes.Gray, x, y);
        }

        if (sbDirty.Length != 0)
        {
          graphics.DrawString(sbDirty.ToString(), m_memoryFont, Brushes.LightGray, x, y);
        }

        if (sbChanged.Length != 0)
        {
          graphics.DrawString(sbChanged.ToString(), m_memoryFont, Brushes.Red, x, y);
        }

        if (sbNormal.Length != 0)
        {
          graphics.DrawString(sbNormal.ToString(), m_memoryFont, Brushes.Black, x, y);
        }

        y += fontHeight;

        lineOffset += (uint)m_pitch;
      }

      //if (false)
      //{
      //  float height = graphics.VisibleClipBounds.Height;

      //  graphics.DrawLine(Pens.Gray, m_renderAddressWidth + characterPadding, 0, m_renderAddressWidth + characterPadding, height);

      //  for (int i = 0; i < m_columns; ++i)
      //  {
      //    float x = m_renderAddressStride + ((float)i * m_renderColumnStride);// +characterPadding;
      //    float x2 = x + m_renderColumnWidth + characterPadding;
      //    graphics.DrawLine(Pens.LightGray, x, 0, x, height);
      //    graphics.DrawLine(Pens.LightGray, x2, 0, x2, height);
      //  }
      //}
    }

    protected override void OnResize(EventArgs e)
    {
      base.OnResize(e);

      m_offset = ClampAddress(m_offset, 0, GetMaximumScrollAddress(m_maxDisplayLines));
      RedrawMemory(true);
      CheckVisibleMemory();
    }

    protected override bool IsInputKey(Keys keyData)
    {
      if ((keyData & Keys.Alt) == Keys.Alt)
      {
        return false;
      }

      switch ((keyData & Keys.KeyCode))
      {
      case Keys.Left:
      case Keys.Up:
      case Keys.Right:
      case Keys.Down:
      case Keys.Prior:
      case Keys.Next:
      case Keys.End:
      case Keys.Home:
      case Keys.Escape:
        return true;

      case Keys.C:
        if ((keyData & (Keys.Alt | Keys.Control | Keys.Shift)) == Keys.Control)
        {
          return true;
        }
        break;
      }

      return base.IsInputKey(keyData);
    }

    protected override void OnKeyDown(KeyEventArgs e)
    {
      base.OnKeyDown(e);

      switch (e.KeyCode)
      {
      case Keys.End:
        Offset = GetMaximumScrollAddress(m_maxDisplayLines);
        m_selectionStart = GetMaximumScrollAddress(0);
        m_selectionLength = 0;
        break;

      case Keys.Home:
        Offset = 0;
        m_selectionStart = 0;
        m_selectionLength = 0;
        m_selectionNibble = 0;
        break;

      case Keys.Left:
        if (e.Shift)
        {
          --m_selectionLength;
        }
        else
        {
          if (m_selectionNibble == 0)
          {
            m_selectionStart = m_selectionStart + m_selectionLength - 1;
          }
          m_selectionLength = 0;
        }
        RedrawMemory(false);
        break;

      case Keys.Right:
        if (e.Shift)
        {
          ++m_selectionLength;
        }
        else
        {
          m_selectionStart = m_selectionStart + m_selectionLength + 1;
          m_selectionLength = 0;
        }
        RedrawMemory(false);
        break;

      case Keys.C:
        if (e.Control)
        {
          byte[] selection = SelectionBytes;
          StringBuilder sb = new StringBuilder(selection.Length * 3); // 2 nibbles and a comma

          for (int i = 0; i < selection.Length; ++i)
          {
            sb.AppendFormat("{0:X2}", selection[i]);
            if (i != (selection.Length - 1))
            {
              sb.Append(',');
            }
          }

          Clipboard.Clear();
          Clipboard.SetText(sb.ToString(), TextDataFormat.Text);
          Clipboard.SetText(sb.ToString(), TextDataFormat.CommaSeparatedValue);
        }
        break;

      case Keys.V:
        if (e.Control)
        {
          string data = Clipboard.GetText(TextDataFormat.CommaSeparatedValue);
          string[] blah = data.Split(new char[] { ',' });

          ushort[] bytes = new ushort[blah.Length];

          for (int i = 0; i < blah.Length; ++i)
          {
            bytes[i] = ushort.Parse(blah[i], System.Globalization.NumberStyles.HexNumber);
          }

          m_memorySpace.UpdateMemory(m_selectionStart, bytes, false);
        }
        break;

      case Keys.Escape:
        m_selectionLength = 0;
        RedrawMemory(false);
        break;
      }
    }

    protected override void OnMouseDown(MouseEventArgs e)
    {
      base.OnMouseDown(e);

      UpdateMousePosition();

      if ((e.Button & System.Windows.Forms.MouseButtons.Left) != 0)
      {
        m_selectionStart = m_hoverAddress;
        m_selectionLength = 1;

        RedrawMemory(true);
      }
    }

    protected override void OnMouseMove(MouseEventArgs e)
    {
      base.OnMouseMove(e);

      int deltaX = m_mousePos.X - e.Location.X;
      int deltaY = m_mousePos.Y - e.Location.Y;

      m_mousePos.X = Math.Max(e.X, 0);
      m_mousePos.Y = Math.Max(e.Y, 0);

      int oldMouseLine = m_hoverLine;
      int oldMouseColumn = m_hoverColumn;

      UpdateMousePosition();

      if ((e.Button & System.Windows.Forms.MouseButtons.Left) != 0)
      {
        if (m_hoverAddress == m_selectionStart)
        {
          // nothing
        }
        else if (m_hoverAddress < m_selectionStart)
        {
          uint newLength = m_selectionStart + m_selectionLength - m_hoverAddress;
          //if (newLength < m_selectionLength)
          //  newLength = m_selectionLength;
          m_selectionStart = m_hoverAddress;
          m_selectionLength = newLength;
        }
        else
        {
          m_selectionLength = m_hoverAddress - m_selectionStart + 1;
        }
      }

      if ((m_hoverLine != oldMouseLine) || (m_hoverColumn != oldMouseColumn))
      {
        RedrawMemory(true);
      }
    }

    protected override void OnMouseWheel(MouseEventArgs e)
    {
      base.OnMouseWheel(e);

      if (e.Delta != 0)
      {
        int lines = -e.Delta / SystemInformation.MouseWheelScrollDelta;

        int delta = lines * m_pitch;

        //m_offset = ClampAddress(m_offset, delta, (uint)(m_memorySpace.AddressMax - ((ulong)m_pitch * (ulong)m_maxDisplayLines)));
        m_offset = ClampAddress(m_offset, delta, GetMaximumScrollAddress(m_maxDisplayLines));

        RedrawMemory(true);
      }
    }

    public void CheckVisibleMemory()
    {
      CalculateSizes();

      GetMemoryEventArgs args = new GetMemoryEventArgs();
      args.entries = new List<MemorySpace.RangeEntry>();

      uint startAddress = (m_offset - (m_offset % (uint)m_pitch));

      uint lineOffset = ClampAddress(startAddress, -m_pitch * m_bufferLines, (uint)(m_memorySpace.AddressMax - (ulong)m_pitch));
      int lines = m_bufferLines + m_maxDisplayLines + m_bufferLines;

      args.entries = m_memorySpace.GetDirtyMemoryRanges(lineOffset, (uint)lines * (uint)m_pitch);

      if (args.entries.Count != 0)
      {
        OnGetMemory(args);
      }
    }

    #region Events
    protected virtual void OnGetMemory(GetMemoryEventArgs e)
    {
      if (GetMemory != null)
      {
        GetMemory(this, e);
      }
    }

    protected virtual void OnMemoryChanged(MemoryChangedEventArgs e)
    {
      if (MemoryChanged != null)
      {
        MemoryChanged(this, e);
      }
    }
    #endregion

    private void timerGetMemory_Tick(object sender, EventArgs e)
    {
      CheckVisibleMemory();
      timerGetMemory.Enabled = false;
    }

    private void CalculateRenderInfo()
    {
      if (m_sectionRenderInfo.Length != m_memorySpace.MemorySections.Count)
      {
        m_sectionRenderInfo = new SectionRenderInfo[m_memorySpace.MemorySections.Count];
        m_renderInfoDirty = true;
      }

      // hack
      m_renderInfoDirty = true;

      if (m_renderInfoDirty)
      {
        int totalLines = 0;
        int totalVisibleLines = 0;

        //uint lastAddress = 0;

        for (int i = 0; i < m_memorySpace.MemorySections.Count; ++i)
        {
          m_sectionRenderInfo[i].startLineIndex = totalVisibleLines;
          m_sectionRenderInfo[i].maxLines = (int)(m_memorySpace.MemorySections[i].size / m_pitch);
          m_sectionRenderInfo[i].visibleLines = m_memorySpace.MemorySections[i].CalculateVisibleLines((uint)m_pitch, (uint)m_pitch, m_renderHidden, m_renderInvalid);

          totalLines += m_sectionRenderInfo[i].maxLines;
          totalVisibleLines += m_sectionRenderInfo[i].visibleLines;

          if (m_renderLabels)
          {
            //if (m_memorySpace.MemorySections[i].StartAddress != lastAddress)
            //{
            //  if (m_renderInvalid)
            //  {
            //    // add lines for invalid section lable
            //    ++totalVisibleLines;
            //    ++totalLines;
            //  }
            //}

            ++totalVisibleLines;
            ++totalLines;
          }
        }

        if (!m_renderInvalid)
        {
          m_maxDataLines = totalVisibleLines;
        }
        else if (m_memorySpace.MemorySections.Count > 0)
        {
          m_maxDataLines = (int)(m_memorySpace.AddressValidMax / (ulong)m_pitch);

          if (m_renderLabels)
          {
            m_maxDataLines += m_memorySpace.MemorySections.Count * 2;
          }
        }
        else
        {
          m_maxDataLines = (int)(m_memorySpace.AddressMax / (ulong)m_pitch);

          if (m_renderLabels)
          {
            m_maxDataLines += m_memorySpace.MemorySections.Count * 2;
          }
        }

        // lines
        {
          if (m_lineRenderInfo.Length < m_maxDisplayLines)
          {
            m_lineRenderInfo = new LineRenderInfo[m_maxDisplayLines];
          }

          uint lineOffset = m_offset - (m_offset % (uint)m_pitch);

          m_maxBits = 8;

          MemorySection section = new MemorySection();

          MemorySpace.ByteInfo[] bytes = new MemorySpace.ByteInfo[m_pitch];

          for (int line = 0; line < m_maxDisplayLines; ++line)
          {
            m_lineRenderInfo[line].address = 0;
            m_lineRenderInfo[line].section = null;
            m_lineRenderInfo[line].invalidSection = false;
            m_lineRenderInfo[line].invalid = true;

            int lineBits = 0;

            bool nextLine = false;
            while (!nextLine)
            {
              if (lineOffset >= m_memorySpace.AddressValidMax)
                break;

              MemorySection newSection;
              MemorySection nextSection;
              m_memorySpace.GetMemorySectionAndNext(lineOffset, out newSection, out nextSection);

              if (m_renderLabels)
              {
                if (newSection != section)
                {
                  m_lineRenderInfo[line].invalid = false;
                  m_lineRenderInfo[line].section = newSection;
                  m_lineRenderInfo[line].invalidSection = (newSection == null);
                  section = newSection;
                  nextLine = true;
                  continue;
                }
              }

              if (newSection == null)
              {
                if (nextSection == null)
                {
                  nextLine = true;
                  continue;
                }
              }

              bool allInvalid = true;
              bool allHidden = true;

              m_memorySpace.GetBytes(lineOffset, ref bytes);

              for (int i = 0; i < bytes.Length; ++i)
              {
                MemorySpace.ByteInfo.Flags flags = bytes[i].flags;

                allInvalid = allInvalid && ((flags & MemorySpace.ByteInfo.Flags.Invalid) != 0);
                allHidden = allHidden && ((flags & MemorySpace.ByteInfo.Flags.Hidden) != 0);
              }

              if (allInvalid && !m_renderInvalid)
              {
                //m_lineRenderInfo[line].address = lineOffset;
                nextLine = false;
              }
              else if ((!allHidden || m_renderHidden) && (!allInvalid || m_renderInvalid))
              {
                m_lineRenderInfo[line].invalid = false;
                m_lineRenderInfo[line].address = lineOffset;
                lineBits = section != null ? section.ByteWidth : 0;
                nextLine = true;
              }
              else
              {
                nextLine = false;
              }

              lineOffset += (uint)m_pitch;
            }

            m_maxBits = Math.Max(m_maxBits, lineBits);
          }
        }
      }
    }

    private uint GetMaximumScrollAddress(int lines)
    {
      CalculateSizes();

      uint address = 0;
      int linesLeft = lines;
      for (int i = m_memorySpace.MemorySections.Count - 1; i >= 0; --i)
      {
        MemorySection section = m_memorySpace.MemorySections[i];
        SectionRenderInfo renderInfo = m_sectionRenderInfo[i];

        if (linesLeft < renderInfo.visibleLines)
        {
          int beginingLines = renderInfo.visibleLines - linesLeft;
          address = section.address + ((uint)beginingLines * (uint)m_pitch);
          break;
        }

        linesLeft -= renderInfo.visibleLines;
      }
      return address;
    }

    private uint GetOffsetFromLine(int line)
    {
      CalculateSizes();

      uint address = 0;
      int linesLeft = line;
      for (int i = 0; i < m_memorySpace.MemorySections.Count; ++i)
      {
        MemorySection section = m_memorySpace.MemorySections[i];
        SectionRenderInfo renderInfo = m_sectionRenderInfo[i];

        if (linesLeft < renderInfo.visibleLines)
        {
          address = section.address + ((uint)linesLeft * (uint)m_pitch);
          break;
        }

        linesLeft -= renderInfo.visibleLines;
      }
      return address;
    }

    private int GetLineFromOffset(uint offset)
    {
      CalculateSizes();
      int line = 0;

      uint address = offset;

      for (int i = 0; i < m_memorySpace.MemorySections.Count; ++i)
      {
        MemorySection section = m_memorySpace.MemorySections[i];
        SectionRenderInfo renderInfo = m_sectionRenderInfo[i];

        if (address < section.EndAddress)
        {
          line += (int)((address - section.StartAddress) / (uint)m_pitch);
          break;
        }

        line += renderInfo.visibleLines;
      }

      return line;
    }

    public static uint ClampAddress(uint address, int delta, uint max)
    {
      if (delta < 0)
      {
        if ((uint)-delta > address)
        {
          address = 0;
        }
        else
        {
          address -= ((uint)-delta);
        }
      }
      else
      {
        if ((address > max) || ((max - address) < (uint)delta))
        {
          address = max;
        }
        else
        {
          address += (uint)delta;
        }
      }

      return address;
    }

    private static char ByteToPrintableChar(byte b)
    {
      char c = Convert.ToChar(b);
      if ((c >= 0x80) || !(char.IsLetterOrDigit(c) || char.IsPunctuation(c) || char.IsSymbol(c) || (c == ' ')))
      {
        c = '.';
      }

      return c;
    }

    private static void FillHexString(ushort val, ref char[] chars, int nibbles)
    {
      if (nibbles == 1)
      {
        int char0 = (val >> 0) & 0xF;
        chars[0] = (char)((char0 > 9) ? (char0 - 10) + 'A' : char0 + '0');
      }
      else if (nibbles == 2)
      {
        int char0 = (val >> 4) & 0xF;
        chars[0] = (char)((char0 > 9) ? (char0 - 10) + 'A' : char0 + '0');
        int char1 = (val >> 0) & 0xF;
        chars[1] = (char)((char1 > 9) ? (char1 - 10) + 'A' : char1 + '0');
      }
      else if (nibbles == 4)
      {
        int char0 = (val >> 12) & 0xF;
        chars[0] = (char)((char0 > 9) ? (char0 - 10) + 'A' : char0 + '0');
        int char1 = (val >> 8) & 0xF;
        chars[1] = (char)((char1 > 9) ? (char1 - 10) + 'A' : char1 + '0');
        int char2 = (val >> 4) & 0xF;
        chars[2] = (char)((char2 > 9) ? (char2 - 10) + 'A' : char2 + '0');
        int char3 = (val >> 0) & 0xF;
        chars[3] = (char)((char3 > 9) ? (char3 - 10) + 'A' : char3 + '0');
      }
      else
      {
        int char0 = (val >> 12) & 0xF;
        chars[0] = (char)((char0 > 9) ? (char0 - 10) + 'A' : char0 + '0');

        ushort temp = val;
        for (int i = nibbles - 1; i >= 0; --i)
        {
          int charI = (temp) & 0xF;
          chars[i] = (char)((charI > 9) ? (charI - 10) + 'A' : charI + '0');
          temp = (ushort)(temp >> 4);
        }
      }
    }

    private void CalculateSizes()
    {
      bool changed = false;

      int oldMaxDisplayLines = m_maxDisplayLines;
      int oldMaxDataLines = m_maxDataLines;
      int oldPitch = m_pitch;

      m_pitch = m_columns * m_grouping;

      if (oldPitch != m_pitch)
        changed = true;


      m_maxDataLines = (int)(m_memorySpace.AddressMax / (ulong)m_pitch);
      m_maxDisplayLines = (Height / MemoryFont.Height) + 1;
      CalculateRenderInfo();


      if ((oldMaxDisplayLines != m_maxDisplayLines) || (oldMaxDataLines != m_maxDataLines))
        changed = true;

      // only do this if it's changed
      if (changed)
      {
        if (m_maxDataLines <= m_maxDisplayLines)
        {
          scrollBarMemory.Enabled = false;
          scrollBarMemory.Value = 0;
          scrollBarMemory.Minimum = 0;
          scrollBarMemory.Maximum = 0;
          scrollBarMemory.LargeChange = 0;
        }
        else
        {
          //scrollBarMemory.Value = 0;
          scrollBarMemory.Minimum = 0;
          scrollBarMemory.Maximum = (m_maxDataLines > 0) ? m_maxDataLines - 1 : 0;
          scrollBarMemory.LargeChange = m_maxDisplayLines;
          scrollBarMemory.Enabled = true;
        }

        UpdateScrollBarPosition();
        RedrawMemory(false);
      }
    }

    public void UpdateMousePosition()
    {
      int nibbles = m_maxBits / 4;

      // TODO: figure out how to get the address the cursor is over

      m_hoverLine = Math.Min(m_mousePos.Y / m_memoryFont.Height, m_maxDisplayLines);
      m_hoverColumn = -1;
      if ((m_characterWidth > 0.0f) && (m_mousePos.X > m_renderAddressStride))
      {
        m_hoverColumn = (int)(((float)m_mousePos.X - m_renderAddressStride) / m_characterWidth);
      }

      int mouseByte = m_hoverColumn / (m_grouping * (nibbles + 1));

      uint oldHoverAddress = m_hoverAddress;
      bool oldHoverAddressValid = m_hoverAddressValid;

      m_hoverAddress = 0;
      m_hoverAddressValid = false;

      if (mouseByte < m_pitch)
      {
        if (!m_lineRenderInfo[m_hoverLine].invalid)
        {
          m_hoverAddress = m_lineRenderInfo[m_hoverLine].address + (uint)mouseByte;
          //Console.WriteLine("0x{0:X8} 0x{1:X2}", m_hoverAddress, mouseByte);
          m_hoverAddressValid = true;
        }
      }

      if ((m_hoverAddressValid != oldHoverAddressValid) || (m_hoverAddress != oldHoverAddress))
      {
        OnHoverAddressChanged(m_hoverAddressValid, m_hoverAddress);
      }
    }

    private void vScrollBarLines_Scroll(object sender, ScrollEventArgs e)
    {
      // hack
      if (!m_renderInvalid && !m_renderHidden)
      {
        Offset = GetOffsetFromLine(e.NewValue);
      }
      else
      {
        Offset = (uint)(e.NewValue * m_pitch);
      }

      timerGetMemory.Enabled = false;
      timerGetMemory.Enabled = true;

      RedrawMemory(true);
    }

    private void UpdateScrollBarPosition()
    {
      if (m_maxDataLines <= m_maxDisplayLines)
      {
        scrollBarMemory.Value = 0;
      }
      else
      {
        uint lineOffset = (uint)(m_offset - (m_offset % (uint)m_pitch));
        int newScrollValue = Math.Min(scrollBarMemory.Maximum, (int)(lineOffset / m_pitch));
        scrollBarMemory.Value = newScrollValue;
      }
    }
  }
}
